package org.jetbrains.exposed.v1.core.statements

import org.jetbrains.exposed.v1.core.*
import kotlin.properties.Delegates

/**
 * Represents the SQL statement that inserts a new row into a table.
 *
 * @param table Table to insert the new row into.
 * @param isIgnore Whether to ignore errors or not.
 * **Note** [isIgnore] is not supported by all vendors. Please check the documentation.
 */
open class InsertStatement<Key : Any>(
    val table: Table,
    val isIgnore: Boolean = false
) : UpdateBuilder<Int>(StatementType.INSERT, listOf(table)) {

    /**
     * The number of rows affected by the insert operation.
     *
     * When returned by a `BatchInsertStatement` or `BatchUpsertStatement`, the returned value is calculated using the
     * sum of the individual values generated by each statement.
     *
     * **Note**: Some vendors support returning the affected-row value of 2 if an existing row is updated by an upsert
     * operation; please check the documentation.
     */
    var insertedCount: Int by Delegates.notNull()

    /** The [ResultRow]s generated by processing the database result set retrieved after executing the statement. */
    var resultedValues: List<ResultRow>? = null
        @InternalApi
        set

    infix operator fun <T> get(column: Column<T>): T {
        val row = resultedValues?.firstOrNull() ?: error("No key generated")
        return row[column]
    }

    infix operator fun <T> get(column: CompositeColumn<T>): T {
        val row = resultedValues?.firstOrNull() ?: error("No key generated")
        return row[column]
    }

    /**
     * Returns the value of a given [column] from the first stored [ResultRow], or `null` if either no results were
     * retrieved from the database or if the column cannot be found in the row.
     */
    fun <T> getOrNull(column: Column<T>): T? = resultedValues?.firstOrNull()?.getOrNull(column)

    @OptIn(InternalApi::class)
    @Suppress("NestedBlockDepth")
    @Deprecated(
        "This function is used in derived classes to build a list of arguments. " +
            "It's recommended to avoid including all default and nullable values in insert statements, " +
            "as these values can often be generated automatically by the database. " +
            "There are no usages of that function inside Exposed. Saved as deprecated for back compatability",
        level = DeprecationLevel.ERROR
    )
    protected open fun valuesAndDefaults(values: Map<Column<*>, Any?> = this.values): Map<Column<*>, Any?> {
        val result = values.toMutableMap()
        targets.forEach { table ->
            table.columns.forEach { column ->
                if ((column.dbDefaultValue != null || column.defaultValueFun != null) && column !in values.keys) {
                    val value = when {
                        column.defaultValueFun != null -> column.defaultValueFun!!()
                        else -> DefaultValueMarker
                    }
                    result[column] = value
                }
            }
        }
        return result
    }

    @Deprecated(
        "This function has been obsolete since version 0.57.0, " +
            "following the removal of default values from insert statements. " +
            "It's safe to remove any overrides of this function from your code.",
        level = DeprecationLevel.ERROR
    )
    protected open fun isColumnValuePreferredFromResultSet(column: Column<*>, value: Any?): Boolean {
        return column.columnType.isAutoInc || value is NextVal<*>
    }

    protected fun clientDefaultColumns() = targets
        // The current check for existing client side without db side default value
        .flatMap { it.columns.filter { column -> column.dbDefaultValue == null && column.defaultValueFun != null } }

    @OptIn(InternalApi::class)
    protected fun valuesAndClientDefaults(values: Map<Column<*>, Any?> = this.values): Map<Column<*>, Any?> {
        val clientDefaultValues = clientDefaultColumns()
            .filter { column -> column !in values.keys }
            .map { column -> column to column.defaultValueFun!!() }

        return clientDefaultValues.toMap() + values
    }

    override fun prepareSQL(transaction: Transaction, prepared: Boolean): String {
        val values = arguments!!.first()
        val sql = values.toSqlString(prepared)
        return transaction.db.dialect.functionProvider
            .insert(isIgnore, table, values.map { it.first }, sql, transaction)
    }

    protected fun List<Pair<Column<*>, Any?>>.toSqlString(prepared: Boolean): String {
        val builder = QueryBuilder(prepared)
        return if (isEmpty()) {
            ""
        } else {
            with(builder) {
                this@toSqlString.appendTo(prefix = "VALUES (", postfix = ")") { (column, value) ->
                    registerArgument(column, value)
                }
                toString()
            }
        }
    }

    open var arguments: List<List<Pair<Column<*>, Any?>>>? = null
        get() = field ?: run {
            listOf(valuesAndClientDefaults().toList())
                .apply { field = this }
        }

    override fun arguments(): List<Iterable<Pair<IColumnType<*>, Any?>>> {
        return arguments?.map { args ->
            val builder = QueryBuilder(true)
            args.filter { (_, value) ->
                value != DefaultValueMarker
            }.forEach { (column, value) ->
                builder.registerArgument(column, value)
            }
            builder.args
        } ?: emptyList()
    }

    /**
     * Returns the list of columns with default values that can not be taken locally.
     * It is the columns defined with `defaultExpression()`, `databaseGenerated()`
     * @suppress
     */
    @InternalApi
    fun columnsWithDatabaseDefaults() = targets
        .flatMap { it.columns }
        .filter { it.defaultValueFun == null && it.dbDefaultValue != null }
}
